perm filename NOTES.OLD[LSP,JRA] blob sn#076955 filedate 1973-12-10 generic text, type C, neo UTF8
COMMENT ⊗   VALID 00006 PAGES
C REC  PAGE   DESCRIPTION
C00001 00001
C00002 00002				NOTES ON A REAL COMPILER
C00012 00003			BINDING AND INTERCOMMUNICATION BETWEEN
C00031 00004				SYSTEMS ORGANIZATION
C00053 00005				NUMBERS IN LISP
C00057 00006			STORAGE STRUCTURES AND EFFICIENCY
C00082 ENDMK
C⊗;
			NOTES ON A REAL COMPILER

	The major failing of the `test compiler' is its total lack of
interest  in variables.  This is  a  non-trivial problem  which every
compiler must  face.    You  have seen  examples  of  plausible  code
generation for simple LISP functions, in particular:

			j[x;y] = f[g[x];h[y]]

The crucial (not crutial) point is how to generate code which `knows'
where on  the stack we can find the value  of a (local) variable when
we execute that code.  What  we shall do is simulate the behavior  of
the runtime  stack while  we are  compiling the  code.   The compiler
cannot  know what the values of the  variables will be at runtime but
we claim that it should know where to find the values.  We will carry
this information through the  compiler in a manner reminiscent of the
`dotted-pair' symbol table of the first version of eval.  Instead  of
posting the current values  in the stack, the compiler  will post the
positions  of the variables relative  to the top of  the stack at the
time we enter  the function.   The variable,  vpr, in London's  paper
(from now on called LP) contains this information.  That is if we are
to compile  a function with λ-variables, [x;y;z] then vpr would begin
with:

			((X . 1), (Y . 2), (Z .3) ...

We also set the offset,  called M, to minus the number  of args added
to vpr. (In this  case -3).  Now if we come across a reference say to
Y while compiling code, we  use assoc [Y ;  vpr] to retrieve 2.   The
offset plus this retrieved value gives  us the relative position of Y
in  the stack.   I. e.,  -3 +  2 = -1.   Thus  to refer  to the stack
location of Y  we could  use (....-1 ,  P).  What  happens as we  add
elements to  the stack? (Or to  be more precise...what  happens as we
generate code which when  executed will add  elements to the  stack.)
Clearly we must modify the  offset.  If we add one  element, we would
set  M to -4.   Then to reference  Y now use  -4 + 2  = -2.   I. e. a
reference to Y is of the form:

			( ......-2 P).

But that's right.  Y  is now further down in the run time  stack.  So
to  complete the analogy, the  `symbol table' is really  defined by M
plus the  current  vpr.    The other  alternative  of  modifying  the
cdr-parts of the entries in the vpr every time we modify the stack is
gross as hell!!
	How do  we make modifications  to the top  of the stack?   We
push new  elements when we are compiling  the arguments to a function
call.  The function  in the LP  compiler which compiles the  argument
list is called complis:

			complis [u;m;vpr] =
				[null [u] → NIL
				T → append [compexp [car [u]; m; vpr];
						((PUSH P 1));
						complis [cdr [u]; m-1; vpr]]]
Notice  that   it  compiles  the   arguments  from  left   to  right,
interspursing  them with (PUSH P  1) and recurring with  a new offset
reflecting the push.
	Here's a brief description of the parts of the LP compiler: 
comp: fn is  the name of  the function to be  compiled.  vars  is the
list of lambda variables.  exp is the lambda-body.
prup:  vars is  a  lambda list,  n  is an  integer.   prup  builds  a
variable-pair list.
mkpush:  generates a sequence of PUSH instructions.
compexp: this  function does most  of the work.   It is  analogous to
eval.
The code for constants is generated by lines [1], [2], and [6].
line [3] generates code for a reference to a variable.
line  [4]  handles  the generation  of  code  for  optimized  Boolean
expressions (the compiler's analog to `more efficient ands and ors').
line [7] is  used for compiling code for  a call on a function.   The
body of line  [7] compiles the argument list with complis, which will
leave the arguments in the stack; loadac loads the  appropriate Ac's.
and then we generate the SUB  to sync the stack, and finally generate
a call on the function.
line [8] you can ignore unless you're really interested.  It compiles
an explicit call on a lambda expression.  The arguments are found and
manipulated directly in the stack rather than being moves to the Acs.
comcond: this compiles the body of conditional expressions.  u is the
p  (sub)i - e (sub)i  list will be  bound to a  generated symbol name
(gensym); M and vpr are as always.  Notice that there is more  than a
passing similarity between comcond and evcond.
combool  and compandor:  are  compile functions  to  try to  generate
reasonable codes for predicates in comcond.
Here is a  transliteration of the  LP compiler's main  functions, and
following  that a specialization  of the  compiler to thhe  subset of
LISP in problem 2.  Then following that a sample compilation of:

  rev = λ[[x;y][null [x] → y; T → rev [cdr [x]; cons [car [x];y]]]]


comp [fn;vars;exp]=
	prog[[n]
	    n ← length [vars];
	return [append [list [LAP;fn;SUBR]];
			mkpush [n;1];
			conpexp [exp; -n; prup [vars;1]]
			list [list [SUB;P;list [C;O;O;n;n]]]
			((POPJ P) NIL) ]]]

prup [vars;n] =
	[null [vars] → NIL
	T → cons [cons [car [vars]; n]; prup [cdr [vars];n+1]]]

mkpush [n;m] =
	[lessp [n;m] → NIL
	T → cons [list [PUSH;p;m]; mkpush [n;m+1]]]



compexp [exp;m;vpr] =
	[null [exp] → ((MOVEI 1 0));
	or [eq [exp;T]; numberp [exp]] → list [list [MOVEI;1;list [QUOTE; exp]]];
	atom [exp] → list [list [MOVE;1;m + cdr [assoc [exp;vpr]]; P]];
	(line 4)
	eq [car [exp]; COND] → comcond [cdr [esp]; m; gensym [ ]; vpr];
	eq [car [exp]; QUOTE] → list [list [MOVEI;1;exp]];
	eq [atom [car [exp]] → λ[[n][append [complis [cdr [exp];m;vpr]
				 	     loadac [1-n;1];
					*    list [list [SUB;P;list [C;O;O;n;n]]
						list [list [CALL;n;
							list [E; car [exp]]]]]
				[length [cdr [exp]]]

line 8

* This is sufficiently mysterious to warrant explanation.  The λ-mess is a 
  function of one argument, n.  It is called, binding n to length [cdr [exp]];

comcond [u;m; ;vpr]=
	[null [u] → list[ ];
	T → λ[[  ] append [combool [caar [u];m;  ;NIL;vpr]
			   compexp [cadar [u];m;vpr]:
			   list [list [JRST; ];  ]
			   comcond [cdr [u];m;  ;vpr]]]
		gensym [ ]]



N.B. same trickery used here as in compexp:   is bound to value of gensym [ ].
		BINDING AND INTERCOMMUNICATION BETWEEN
		  COMPILED AND INTERPRETED FUNCTIONS

               Global Variables and Functional Arguments

	The models of compilation which we have sketched so far store
their  lambda  variables  in  the  stack,  P.   References  to  those
variables in  the body of  the lambda  expression are  made to  those
stack entries.  This scheme suffices  only for lambda or prog (local)
variables.   We have said that labmda expressions may refer to global
or free  variables.  The  lookup mechanism  simply finds the  current
binding of  that global in the  symbol table.  Notice that  this is a
different strategy than the global lookup of Algol.  In Algol, when a
procedurerefers to a global we take the  binding which was current at
the  point of definition  of the procedure.   The  LISP rmechanism is
called dynamic binding.   It corrresponds to physically  substituting
the body  of the definition  of the  function wherever it  is called.
Its model  of implementation is simpler than that required for Algol.
We don't need the static chain for this case of  global lookup.  Thus
interpreted expressions encounter  no problems when faced with global
variables.  There are potential  difficulties for compiled code.   If
all we store  of the stack in a  compiled sunction is the value  of a
variable  then another  program which  expects  to use  that variable
globally will have no way of  finding that stored value.  One  scheme
is to store  pairs on the stack:  name and value (LISP  1.85) then we
can  search the stack for  the latest binding.  It  works.  With this
scheme we  can dispense  with the  VALUE cell.  Actually this  scheme
isn't  all that bad.   The  compiler can still  `know' where  all the
local variables  are on  the stack  and  can be  a bit  clever  about
searching for  the  globals.   Notice this  is the  old symbol  table
mechanism  (a-list)  again.   LISP  1.85 was  designed  for  a paging
machine (XDS 940) and a  few unpleasant features crept in because  of
this.   (However, on  the positive  side some  nice coding  tricks to
minimize page references also arose.)
	The solution proposed  by the LISP 1.6  implementation called
shallow  binding, allows  the compiled  code  to directly  access the
VALUE-cell in the symbol table.  There is an artifcat in the compiler
(and  assmbler) called  SPECIAL.    Variables which  are  to be  used
globally  are declared special variables.   When a  variable say X is
declared special the compiler will emit a reference to X as (MOVE ACi
(SPECIAL X)) or MOVEM  ACi (SPECIAL X)) rather than the corresponding
reference to a location on the  stack.  When the LISP assembler  sees
the indicator SPECIAL, it will search the  symbol table for the VALUE
cell of X and  assemble a reference to that cell.  Since the location
of the value  cell does not  change, we can  always find the  current
binding posted  in the  cdr-part of  that cell.  Notice too  that any
interpreted function  can also sample the VALUE cell so global values
can be passed between compiled and interpreted functions.  The values
of local variables are still posted on the stack.
	We have not yet  discussed the mechanism which will  allow us
to  pass back and  forth between compiled  and interpreted functions.
That is,  compiled can  call interpreted  and conversely.   First  we
require that the  calling conventions for both kinds  of functions be
isomorphic.
	When an interpreted function calls  a compiled (or primitive)
function, eval will  look for the indicator,  SUBR; then retrieve the
machine address of the code and enter via a PUSHJ.Mumble, no problem.
	Compiled functions  call other functions  via the (CALL  n (E
fn))  artifact.  The Call  opcode is actually  an illegal instruction
which is trapped to a submonitor inside eval.  This  submonitor looks
down the  property list  of the  atom, fn,  for a  function indicator
(SUBR,  EXPR,  etc).  The  function is  called  and  control  is then
returned to  the address  immediately following  the CALL.   In  many
cases this Call can be replaced  by a (PUSHJ P fn), but not always as
we will see next.

		    Functional Arguments

A functional argument is  a variable (lambda or prog  variable) whose
value  will be a  function (a name  or lambda-expression).   You have
already seen a very simple case of this:

	λ[[x;y] x[y]] [CAR; (A B)] or
	   ((LAMBDA (X Y) (X Y)) (QUOTE CAR) (QUOTE (A B)))

The variable, x, is a functional argument.  If you did go through the
execution of this  example you found that `the right thing happened'.
That is, we finally ended up applying he car function to the list, (A
B).  Notice that we assigned a value to the functional argument using
QUOTE.     This  is  not  by  accident.  Recall  that  LISP  performs
call-by-value.   What  we want  to pass  to the  body  of the  lambda
expression is not the value of car but the name of the function.  How
do we stop  evaluation of an  argument??   We quote it!!   The  world
isn't quite this simple.  The quote operator does not suffice for all
applications of functional arguments.  Consider the following strange
function.

	tester = λ[[ ;fn] [null[ ] → NIL;atom[ ] → fn];
		 T → tester [car[ ];`λ[[]tester[cdr[ ];fn]]']]
(notice, we are using `...' to signify quoting.)

and a call  on tester: tester  [(A(B) (C));`λ[[] foo [A]]']  thus: is
bound to be (A (B) (C)), fn is bound to `λ[[]foo[A]]', p (sub)1 and p
(sub)2 are false and thus we take the `otherwise' branch.   We rebind
fn  to `λ[[]tester[cdr[  ]'fn]' and  lose big.   When  we attempt  to
evaluate cdr  [ ] we get the wrong !  The environment at the point of
call in e (sub)3 should have been associated with  fn when we rebound
fn. That is, we must  associate the symbol table which was current at
the point of call with the functional argument.  This solution  might
seem a bit  drastic, but it is  easy to generate counter  examples to
less sophisticated implementations.
	What does  this say  about functions?   We  already say  that
functions are  parametric values; to that we  add: functions are also
tied to the environment  in which they  were created; they cannot  be
evaluated in vacuo.   (Notice that the problem goes  away if we don't
have   global  variables.)   The  device   LISP  uses   to  associate
environments with functions is called FUNARG.  Instead of designating
a `value' which is to be  used as a function by the QUOTE operator we
use the indicator, FUNCTION.  Thus:
 
	function [λ[[] tester[cdr [ ]; fn]]] or
	(FUNCTION (LAMBDA ()(TESTER (CDR L FN)))

When eval sees  the indicator (FUNCTION  fn) it returns as  value the
list:

	(FUNARG fn current-symbol table).

When eval (or actually  apply) sees (FUNARG fn st), that  is, when we
are calling gn, we use theol table, for accessing global variables.
	It should be apparent that (QUOTE fn)  and (FUNCTION fn) will
have the same effect if fn has no global (or free variables).
	This seems to be  a lot of work simply to  allow a moderately
obscure  construct to  appear in  the  language.   This is  not true.
Constructs like  functional  arguments  appear  in  most  programming
languages  under different  names (think  about it)  but in  LISP the
problem and the solution appear with exceptional clarity.  Also, in a
more sophisticated  vein, functional  arguments may  be exploited  to
describe  very general  kinds  of control  structures  in programming
languages.   Co-routines,  back-tracking,  and  multi-processing  are
modeled very naturally as applications of functional arguments.  What
does this say about  the CALL mechanism n the compiler?  It says that
the calling  mechanism  for  a  functional argument  must  always  be
trapped the submonitor and decoded.  We cannot replace that call with
a  PUSHJ to some machine  language code for  the function because the
function referred to can chage.  We actually use  a CALLF instruction
to designate a call on a functional argument.
	
		Tracing and debugging in LISP

	When can the  CALL instruction be  replaced by a PUSHJ?   The
PUSHJ  instruction  is  used to  call  a  machine language  function.
Obviously  if  we  are  calling  an  interpreted  function,(  it  has
indicator EXPR  of FEXPR) PUSHJ  is the wrong thing  to do.   In this
case  we must pass  control to  eval, evaluate the  function with the
appropriate arguments,   return the value  in AC1 and finally  return
control to the instruction following the CALL.  If the function being
called is a  machine language  routine (indicator is  SUBR or  FSUBR)
then  we  could  replace   the  CALL  with  a  PUSHJ.     This  would
`short-circuit'  the call on  the submonitor  and the calling  of the
function could be  done more quickly.   There  are many occasions  in
which we do not wish to make this replacement, however.
	Assume for  the  moment that  I am  mortal and  that my  LISP
program  has some  bugs.   Crutial  pieces of  information  about the
behavior of the program are: which functions are being  entered, what
are  the actual  parameters, and what are the  values being returned.
Assume that  we wish to monitor the behavior of the function, FOO. We
will hide the  real definition of FOO  on another symbol table  entry
(using gengym[])  and redefine FOO such  that, when it  is called, it
will:

	1.  print the values of the current actual parameters.
	2.  use  apply  to call  the real  definion of  FOO with  the
	    current parameters.
	3.  print the value of the call on FOO.
	4.  return control to the calling program.

This device is called tracing.
	Since  FOO  may  be  recursive  we   should  also  give  some
indication of the  depth of recursion being executed.  Now every call
on FOO will give us  the pertinent statistics.  Interpreted calls  on
FOO will  go through eval, and  if (CALL...FOO) is being  used in the
compiled  code  the  submonitor  can  pass  control  to  the  tracing
mechanism; but if the CALL is replaced by a PUSHJ, the control passes
directly to the machine language code for FOO and we cannot intercept
the call.
	On most implementations  of LISP the PUSHJ-Call  mechanism is
under  the  control   of  the  programmer.    After  the  program  is
sufficiently debugged,  replace  the  CALL  with the  PUSHJ  and  the
programs will  execute faster.   But  be warned  that this  action is
irreversible; one the CALLs have been overwritten it's tough beans!!
	A variant of this tracing scheme can  be used to monitor SETs
and  SETQs in  interpreted progs.   Since calls  on SET and  SETQ are
interpreted (by eval  and Co.),  we can modify  their definitions  to
print the  name of the  variable and the  new assignment, do  it, and
return.  (question: can you see why this perhaps won't (or shouldn't)
work for compiled code?) Much more sophisticated debugging techniques
can  be implemented,  particularly  in an  on-line  implementation of
LISP.

		Example of a Useful Functional Argument

	The  function,  mapcar  [x  ;  fn],  is  a  function  of  two
arguments.   fn is a functional  argumet.  x is a  list, (x (sub)1, x
(sub)2...,x (sub)n).   the  value returned  is a  list consisting  of
(fn[x  (sub)1], fn[x  (sub)2],...,fn[x (sub)n]).  mapcar can  thus be
defined as:

	mapcar = λ[x ; fn][null [x] → NIL
			T → cons[fn[car [x]];mapcar[cdr [x];fn]]]]

for example:

	λ[[j] mapcar [j ; function [add1]]][(0,1,2)] = (1,2,3)

or in the new definition of apply:

	evlis [x] = mapcar [x ; function [eval]]
			SYSTEMS ORGANIZATION

	There are  many applications of  data structures  which arise
very  naturally in the domain  of systems programming.   This section
will begin  with  a  very  brief historical  description  of  systems
organization, from the bare machine to multi-processing.
	In  the  early  days of  computers,  operating  systems  were
non-existent.   You would  sign up for  a couple of  hours of machine
time, appear  with your  card deck  or paper  tape,  and operate  the
machine.  Debugging devices consisted of a sharp pointed stick, and a
quick  hand on the console  switches.  This  means of programming was
very satifying to  many of the  programmers, but machine  utilization
left something  to be desired.   Much of  the time the  machine would
sitt idle (stopped) as  you would think  about what to  do next.   As
processing speed and  machine costs increased it became  evident that
this  mode  of operation  had to  go.   The  first  operating systems
consisted of a dull  slow object called  an operator and a  satellite
computer on which an input tape  consisting of many separate jobs was
created.   Each  job  on the  input tape  was swquentially  read into
memory, executed and the output presented to an output tape.  If some
abnormal  behavior  was  detected  in  your program,  you  were  also
presented with an  often uninspiring  octal dump of  the contents  of
memory. Whenever a job required say, a  data tape, the operator would
be notified,  and the machine would wait  until the tape was located,
mounted, and readied.  There was a moderately small  resident monitor
which controlled  the input, output  and perhaps the timing  of jobs.
Gross  utilization of the machine was  increased, however at the cost
of  easy  debugging.    This  mode  of  operation   is  called  batch
processing.
	The CPU  (central processing  unit) still  locks up when  I/O
devices  aren't ready and the  user can physically  halt the machine.
For this  model and  most  of the  future discussion,  it  simplifies
matters to  assume that the  monitor resides  in a separate  core box
from that which contains user programs.

Thus:









The  user programs  are  loaded  into  explicit (hard)  locations  in
memory,  and it is  assumed that  the user has  access to all  of the
addressing space of his core box.
	In  an  attempt  to decrease  the  overhead  on  waiting  for
external actions (I/O  requests or operator intervention) we attach a
reasonably  fast  storage  device  (and  increase  the  size  of  the
monitor).  This storage device is  capable of holding many user jobs.
Whenever  a user (loser)  program requires a service  which cannot be
satisfied immediately,  the  monitor will  swap  the program  out  of
memory onto  this storage device, initiate action  which will tend to
satisfy the request, and bring  another job into core, beginning  its
execution.   This new job  may either  be the next  job on the  input
stream, or  a job which was swapped out and  now is ready to run. The
user still is given  the whole addressing  space, he is still  loaded
into  explicit memory  locations, but  the monitor  must now  be more
cleaver.   It must  be able  to recognize (or  `trap') requests which
would lock up the CPU.  It must be able to make decisions as to which
job to run next.











Clearly  there  are   grossinefficiencies  in  the   present  scheme.
Allowing, or in  fact requiring, that each user have access to all of
memory is too generous.  Assume that our memory size is 32→ words and
assume that we  have 16K jobs which  are ready to run.   We should be
able  to load one job  into locations 0 - (16K-1)  and load the other
job into the  other half of memory.   When one job  requires external
service, we  should be able to  switch the CPU to  begin execution of
the other job provided that we save all information about the current
state of the suspended jog.  The point is  that it takes time to swap
jobs in and out  of core so thaat anything we can do to decrease this
overhead is  a winner.   What  are the  added  requirements for  this
scheme?  Manily we must have some  scheme for keeping the jobs out of
each  other's hair.  This  is usually done by  a hardware addition to
the machine  called the protection  register.   Do typists read  what
they are typing??  No it's a bunch of crap!!!

		[CRAP (1)] ≡ [LISP] sexpr
		N. Meller has a bad attitude.

In this simple scheme the protection register  would be loaded by the
monitor  with the  upper  and lower  bounds on  the  addressing space
accessible to the current job.  Then every memory reference made by a
program is filtered  through the protection register.   Obviously the
instruction  to  load  protection register  should  be  restricted to
execution by the monitor.
	What are  the inefficiencies in  this scheme?   Consider what
happens  when  we have  to  swap a  job  out of  memory.    Since the
addresses  used  by  the  program  are  explicitly  tied   to  memory
locations, we must swap that job  back into exacly the same locations
if  it is  to run correctly.   Consider  two 16K  programs whose only
effect is to execute `jump-self' loops ( L (JRST  L) ). Their initial
load might look like:







If we swap out the top job and try to swap it back into the lower 16K
at some later time, we lose big.








But clearly if the bottom  16K is available we should be  able to use
it.  We want to be able to swap our programs into any available block
of core which is of sufficient size.
	 To  accomplish  this we  add a  new piece  of hardware,  the
relocation register. Now our loader loads all our programs as if they
begin at location, 0.  Thus in the above example:








To get the  appropriate effect when  we execute any program,  we bias
every  memory reference by  actual address assigned  to the program's
location 0.   This  bias is  put in  the relocation  register by  the
monitor before it begins execution of  the program.  With this scheme
we  can run any jobs in any block  of core which is the correct size.
All we  need do  is load  the  appropriate offset  in the  relocation
register.








	Now we can dispense with the two-core box approach.  Just use
one box and bias  every access in a user program by the length of the
monitor plus the usual offset:









The actual harrdware can also be simplified now since the lower bound
on every user's addressing space is always zero.
	Clearly, more refinements  need to be  made.  At  present the
only way  to interrupt a job is if he  terminates,  or calls for some
external action, or  attempts to perform  some illegal or  restricted
instruction.   Any operating system needs  to be able to  monitor the
amount of  CPU time spent on a job.  So we should have a programmable
clock  which can  be  set  by the  monitor  and  which will  send  an
interrupt to the at a  specified time.  Actually other hardware needs
to  be  added  to  our  entourage  if  we  intend  to  do  reasonable
time-sharing.  A  sophisticated   priority  interrupt  system   is  a
necessity.  Since  many of the applications of data structures appear
in time-sharing systems, we will say a bit about the behavior of such
systems.

		   Time-sharing organization

	Though certain ding-dongs  are still publishing  papers about
whether time-sharing  is a useful way o  programming, I feel that the
question has been answered in the affirmative for many years. Period.
	What make  time-sharing viable  is the tremendous  difference
between human reaction time and machine speeds.  In a period of a few
seconds a well  designed t-s  system can satisfy  simple requests  by
many users.   If  a user must  wait a significant  length of  time to
obtain  a  response  to a  simple  command, then  your  system  is in
trouble.   The heart  of a  rapid  response is  a priority  interrupt
system.
	A time-sharing monitor  must service terminals,  mass storage
devices and control the dynamic allocation of its memories.
	Consider the  care and feeding  of a relatively  fast storage
device,  say  a  disk,  and  its  interaction  with the  sending  and
receiving of  characters from user  terminals. Most  disks require  a
sequence of  commands be  issued before an  actual data  transfer can
take  place.  Assume that there are two  commands: a seek to find the
appropriate area on the device, followed by the transfer command.  If
the CPU were tied up from the beginning of the seek to the end of the
transfer, significant  amounts of  CPU time  would be  lost.   If  we
issued the  seek, went  off to  do other  calculations, but were  not
responsive  enough when  the  seek was  completed we  might  miss the
chance to make the transfer due to intervening rotation of  the disk.
What we can do  is put the disk on a  high priority interrupt channel
and  the terminal keyboards on a medium  priority channel.  Issue the
seek, go back  to computation, perhaps  servicing the keyboards;  and
when the disk  is in position we will get  a high priority interrupt,
freezing the  state  of the  computation;  at this  time,  begin  the
transfer of information, dismissing the  interrupt and continuing the
computation.  Thus higher priority requests can interrupt lower ones;
any  lower priority  requests must  be suspended  or saved  until the
higher one is completed.  You might decide to design the  hardware to
allow recursive interrupts  on a particular channel  or might require
completion  before another  request can  be serviced.   (What  do you
think?)
	What about  the  data structures  used in  a complex  system.
Data structures  are used heavily in the  scheduling algorithms.  the
t-s monitor must make decisions about which jobs in the system are to
run.   These decisions are  based on such  simple things as:  the job
isn't  requesting service--dormant; the  job is waiting  for some I/O
device--I/O  wait; or  decisions  must  be based  on  more  difficult
criteria: the  job is ready to  run but is too big  for the currently
available allotment of core;  there is a job  ready which needs  fast
response or has been waiting inordinately long for service.
	In  general a  vast  array  of  information can  be  used  in
deciding which  job to run next.   Usually these decisions take shape
by reqquiring that  the jobs currently in  the system be  partitioned
into  a  finite  set  of  waiting-lines  or  queues.  The  scheduling
algorithm manipulates  these queues as the status of the jobs change.
A queue  is  a  data structure.    Queues  are also  called  first-in
first-out lists (FIFO) or pop-up  lists.  In LISP parlance you append
new queue-elements to the end of  the list and take the elements  off
of the  front of  the  list.   It should  be obvious  where the  name
waiting-line comes from.
	Queues are also used in the buffering schemes of I/O devices.
As we type `LOGIN.....'  to a t-s system, it is usually the case that
the character string first goes into a system buffer and when the CPU
is free,  the command is decoded.   Obviously we want  the monitor to
see  the character  string in the  same order  in which  we typed it.
That is the buffer is acting like a queue.
	A queue can be implemented as simply a list with a pointer to
the front,  and a pointer to the end.   When you add something to the
end, you update one  pointer; wen you take  an element off the  front
you update  the other pointer.   There is  the obvious check  to make
sure  that you can recognize the empty  queue: when the front pointer
meets the end pointer:





As with  stacks, the queue  is usually not  represented as a  list of
argitrary length.  A queue can be represented  as a sequential set of
memory locations, still with two pointers.  What happens when we have
filled the  last cell in  the sequence.  Well,   if some  process has
been removing items from the queue, then the front pointer has moved:




In this case  the cells from  the first cell  in the sequence  to the
current position of  the front pointer are available.  Use `em.  That
is a queue is usually implemented as a sequential circular  list with
two pointers.   In this implementation the tests for  the empty queue
are  slightly more complex, but are still  obvious.  Notice too, that
we must now also test for the full queue.
	In  system applications  it  is  usually  the case  that  one
process is filling the queue and one process is emptying it.  In this
application, a full  queue should simply put  the filling process  to
`sleep' (or suspend it), and when a queue becomes empty, the emptying
process  should  be  suspended.    There  are  some  interesting  and
non-trivial  problems   which   arise   in  connection   which   such
`cooperating processes'.
	There are some interesting data  structure applications which
are connected with  the allocationa dn (?) liberation of storage. The
monitor  must  be  able  to   allocate  storage  to  jobs  as   their
requirements become known  and must also be able to  recover areas of
memory which  are no longer being used.  Similarly, since an integral
part of a large system is a file structure, the  monitor must be able
to handle  the demands of file  maintenance.  These  two problems are
related  but  solutions  for  core  allocation  are  not  necessarily
reasonable ones for file allocation.
			NUMBERS IN LISP

	Inums, Fixnum, Flonums, and Bignums
On most versions of LISP, numbers are stored  as very simple kinds of
atoms:  they do not  need print names,  but since  we should probably
allow fixed and floating point representation, we do  need indicators
for these properties.  Thus:







Notice that each actual number is stored in FWS.  This representation
is a  bit expensive.  On  some machines we can use  a coding trick to
improve representation of some  numbers.  Assume that the  addressing
space of the machine  is 2 (sup)18 and that the usual  size of a LISP
core image is significantly smaller, say, N.  Then all memory address
references greater than N  are illegal (and trapped by  the monitor).
What we  will do is use  these illegal address to encode  some of the
smaller positive and  negative integers, mapping  zero on the  middle
address, the  positive numbers to  lower addresses and  the negatives
onto  the  higher   addresses.    Thus  these  smaller  integers  are
represented by pointers outside of the normal LISP  addressing space.
This  trick can  considerably decrease  the storage  requirements for
jobs  heavily using small  numbers.  (Numbers are  not usually stored
uniquely).











Most  numerically oriented  programs  are  faced  at some  time  with
overflow.  That is, they attempt  to construct a number  which is too
large to  be represented  in one  machine location.    There are  now
several   versions   of   LISP  which   will   automatically   change
representation  when faced  with overflow.   This  new representation
called Bignums, could have the following structure:









The value of Bignum is given by:

 
where α-1 is  the largest number  representable in one  machine word.
The intertranslations  between Inums, Fixnums or Flonums, and Bignums
is done automatically by eval and Co.
		STORAGE STRUCTURES AND EFFICIENCY

	Though it is true that any algorithm can be coded in terms of
manipulations of binary trees, there are many instances in which more
efficient organizations of data exit.
	At the most elementary  level are vectors and arrays.   These
data structures could  certainly be stored in a list structure format
and individual components accessed via car-cdr chains.  However, most
machines  have a  hardware  organization which  can  be exploited  to
increase   accessing   efficiency  over   the   list  representation.
Similarly, strings can  be represented as  lists of characters.   The
string processing operations are expressible as LISP algorithms.  But
again this is usually not the most resonable representation. Even  at
the level of list-structure operations, simple binary trees might not
be the most  expeditious representation for every problem.  Also many
of the algorithms we  have presented in  LISP are overly wasteful  of
computation time.  This section of notes will begin an examination of
alternatives   to  LISP  organization.     There  is   no  best  data
representation, or no best algorithm.  The representations you choose
must match your machine and  the problem domain you are studying.  If
your application is strictly numerical then list-structure is not the
answer; if you wish to manipulate simple linear strings of characters
then list processing is too general.  And there are many applications
of list processing which are sufficiently well-behaved that you don't
need a storage  allocation device as complex as  a garbage collector.
The point  is that if you have seen the list-processing techniques in
a rich environment  such as  LISP and have  seen the applications  to
which  LISP may  be put,  then you  will be  prepared to  apply these
techniques  in  a  meaningful  way.    Many  times  an  (inefficient)
representation in  LISP  is all  that is  needed.   You  get a  clean
representation  with comprehensible algorithms.   Once you've studied
the algorithms, efficiencies might come to mind. At that  time either
recode the problem using some  of the obscene LISP programming tricks
or drop into machine language if very fine control is required.  Once
you have  a  representation it  is  easy to  get  better ones.    For
example,  one you have  a compiler  (which is  proved correct)  it is
easier to describe a smarter one.
		
			Storage Structures

	Bit tables: In the marking phase of a garbage collector it is
necessary to  record the marking of each word.   On many machines the
marking of a word is signified by setting a bit in a bit table or bit
map.  For example:












This  might be  done for  several  reasons.   The  natural choice  of
setting  a mark-  bit  in the  actual word  being  marked may  not be
possible of no  the best strategy: 1)  for words in  FS, there is  no
room if each  word contains exactly two addresses; or  2) the word is
in  FWS and  the  meaning of  the information  stored there  would be
changed;  3) also  the  garbage  collector must  initialize  all  the
mark-bits to  zero before the actual marking  process begins (look at
the g.c. algorithm).  It is faster  on most machines to zero a  whole
table rather than zero single bits in  separate words; and finally 4)
in  garbage collectors for more  complicated data structures, marking
with a bit table becomes a necessity.
	Vectors:  Vectors (or  one  dimensional arrays)  are  usually
stored sequentially in memory.  Simple vectors are usually stored one
element to a  memory location though  this is not  a necessity.   For
example a complex vector is very  naturally stored as pairs of cells;
or if  perhaps you would allow vectors of non-homogeneous data modes,
each element would  have to include type  information.  In any  case,
most languages make some restrictions on the behavior of vectors such
that efficient accessing of elements can be made.  There is a natural
simulation of  a stack as  a (sequential) vector  with access  to the
stack made via a global pointer to the vector.
	Arrays: Arrays are multi-dimensional data  structures.  Since
most  machine memories are linear  devices we must map  arrays onto a
linear representation.   We will  restrict attention fo  the case  of
two-dimensional  arrays.   Most  of the  discussion generalizes  very
naturally.   A very common device is to store the array by rows; that
is,  each  row is stored sequentially,  first, row 1; then  row 2,...
Given this representation there  is a trivial calculation to find the
location of an arbitrary element, A[i;j], if we know the location  of
the first  element, A[1;1] and  the extent of  the dimensions  of the
array.  For an array A[1:M; 1:N]

		loc[A[i;j]] = loc [a[1;1]] + (i-1)*N + (j-1)

In languages like Fortran which require that the size of the array be
known at compiler-timer  the compiler can generate the accessing code
without problem.  Languages, like Algol 60, and some versions of LISP
which allow the size of an  array to be determined at runtime require
a bit more care.  Algol 60, for example requires that you declare the
type (real, boolean,  etc.) of  the array and  specify the number  of
dimensions  in the  array,  but you  can postpone  until  runtime the
designation of the size of each dimension.  To handle this complexity
a dope vector is  introduced. The compiler can determine  the size of
the dope vector, but  not the contents.  The dope vector is filled in
at runtime and  contains information about the  actual extent of  the
array bounds.   Also since  the size of the  array is not  known, the
compiler  cannot   allocate  space  for  the  array  elements.    The
allocation must be  done at runtime.   When the array declaration  is
executed we must  know all the information about the  array.  At that
time we add the  array-bound information to the  dope vector and  add
information telling where  to find the  array elements.   Assume that
the  array elements are stored  by rows.  Look  at the calculation of
the location of element, A[i;j].   For specific execution ofan  array
declaration much  of this information  is constatnt; the  location of
the array  elements, in particular, A[1;1] and the number of columns,
N, is fixed.  Thus we rewrite the calculation as:

		constant part			variable part

		[loc [A[1;1]]-N-1]       +   	(i*N+j)

The constatnt  part is sored  in the  dope vector.   When we wish  to
reference  an element A[i;j] we  need only compute  the variable part
and add it to the constant part.
	The dope vector for A [1:M; 1:N] perhaps might contain












	There is another scheme  for storing arrays which is  used in
some of  the Burroughs machines. Each row  is stored sequentially and
access  to  separate  rows  is   made  through  a  device  called   a
`mother-vector'.   The mother vector is  a vector of pointers  to the
rows.  Thus:










Notice that the accessing computation is very  cheap.  Another effect
is  that all rows  need not  be in memory  at once.   On a  paging or
segmenting machine (we  will talk  about machine organization  later)
this array organization can be helpful.  If an access to a row not in
core  is made, a `page  fault' is raised; the  monitor brings the row
into memory and the computation continues.   The mother-vector scheme
generalizes  nicely  to   multidimensionality  and  can  be  used  in
conjunction with a dope vector.
	Strings: Strings and string processors are an important class
of data  structures and algorithms.  Powerful  string processing is a
necessity  for any  well  developed  compiler-writing  system.    The
organization  and  implementation  of   a  general  string  processor
directly parallels  (you guessed it) LISP.  In fact a subset of LISP,
linear LISP,  is a nice notation for string algorithms.
	A string is a sequence of characters.   A reasonable language
(not PL/1) for string processing should allow the creation of strings
of arbitrary length at runtime; it should allow the generation of new
strings and the  decomposition of existing  strings.  I f  strings of
arbitrary length are  to be created, an organization similar to FS in
LISP can be used with, perhaps, a string garbage collector.   We will
assume this most general case.
	The machine memory will  contain at least a string  space, an
evaluator, a symbol table, and a garbage collector.
	String space is a linear sequence of cells, each of which can
contain exactly  one charcter.   A string  will be  represented as  a
sequence of sequential character cells.  A string variable will be an
entry in  the symbol table; the current value of the variable will be
represented as a pair; character count and a pointer to the beginning
of the character sequence.

Thus:





We must make some decisions about how  we manipulate strings: when we
perform x←y, do we copy the symbol table pair of y into that of x, or
do we make a new string isomorphic to y and point x at it.   It makes
a  difference.    We  will   choose  the  former,  copying  only  the
`descriptor',  that  is,  we  will  share  strings  (and  substrings)
wherever possible. This decision makes the  storage requirements less
expensive, but will make our  life more difficult when we worry about
garbage  collection.   There are  three  primitive functions:  first,
rest,  concat  (read: car,  cdr,   cons).    first [x]  is  the first
character of the string represented by x.  first is undefined for the
empty string. For example:

		first [ABC] is A; first [∧] is undefined

rest[x] is  the string  of characters  which remains  when the  first
character of  the string is deleted.  rest  is also undefined for the
empty string. For example:

		rest [ABC] is BC

concat [x;y] is a function of two arguments.   x is a character; y is
a  string. concat forms  a string  consisting of the  concatenation a
copy of x and a copy of the string, y.  For example:

		concat [A ; BC] is ABC
	
There are three string predicates:
	char [x]:  is x a single character?
	null [x]:  is x the empty string?
	x = y:  are x and y the same character?
For example:

		char [A] is true
		char [AB] is false
		AB = AB is undefined

	Now to implementations:
	first generates a character  count of 1 and a  pointer to the
first character of the parent string.
	rest generates a character count of one less than that of the
parent and a pointer to the second character of the parent string.
	concat is a  bit more  problematic.   We copy x  and copy  y,
generate  a character  count of  the sum  of those  of x  and y,  and
generate a pointer to the character of the copy of x.  The copies are
made in the free string space pointed to by the string space pointer.
The obvious  question of  what to do  when string space  is exhausted
will be postponed  for a moment.   The implementations  of the  three
predicates are easy: we will blur  the distinction between characters
and strings  of length 1.  Thus char  need check the character count.
null says character count is 0.  What about = ?  Obviously characters
are  not  stored   uniquely,  so  we must  make  an  actual character
comparison.
	Now  garbage collection.    In  some  ways a  string  garbage
collector is simpler and in some ways more difficult than a collector
of list-structure.   The  marking phase  is much  simpler: using  the
descriptor in the symbol table, mark  the character string.  Since we
are sharing  substrings, we cannot stop the marking simply because we
have encountered a previously  marked character.  (think  about it!!)
But at least  the marking is not recursive.   However, the collection
phase needs to be  more sophisticated for  string processors.   Since
strings are  stored linearly (or  sequentially), a  fragmented string
space  list  is  of  little  use.    Thus  we  must compact  all  the
referenceable strings into one end of string space, freeing  a linear
block for the new free string space.  Since we are sharing substrings
a  little care  needs  to  be exercised.    When  we move  a  string,
obviously the descriptor of any variable referencing any part of that
parent string must be changed to reflect the new location.  So before
we begin the relocation of strings  we sort the string descriptors on
the  basis of their  pointers into  string space.   We then recognize
each parent string, moving it down into freed  locations and updating
the address  pointers of any  substrings.  We  continue this process.
Eventually all strings will  be compacted down  in string space.  The
free space  pointer will  be set  and the  computation can  continue.
Compacting garbage collectors  can be adapted for use in LISP or more
general types of data structures.  (Think about this)
	List  processing: We  will  first look  at  some LISP  coding
tricks which when used judiciously, can increase efficiency, but when
used in a cavalier manner can result in mystery.  First, LISP does an
awful lot of copying.  Consider:

	append [x;y] = [null [x] →T→cons [car [x]; append [cdr[x];y]]]

This function copies x onto the front of y.   (note: y is not copied)
Or look  at the subst function: it generates  a copy with the correct
substitutions made.   The copying is necessary  in general. It  keeps
unsavory side effects from happening.
	Let's look at append [(A B C) ; (D E F)].  It appears that we
could get the appropriate effect of append by `cdr-ing' down the list
(A B C) until we found the terminator, then replace it with a pointer
to the list (D E F).  Thus:




What's wrong here?  Consider the sequence of statements:

		i ← (A,B,C)

		j ← (D,E,F)

		k ← append [i;j]

Then if append worked  as advertised above, (changing the  cdr of the
last element of i) the following evil would be perpetrated: the value
of  i  has  been  chaged  surreptitiously!!  i  now  has   the  value
(A,B,C,D,E,F).   Language features which  do this  are evil.   It is,
however,  quite useful to be  evil sometimes.   Notice that any value
which was sharing  part of the structure  of i will also  be changed.
This can cause real mystery!! Well the world is good, and append does
not work as above.   The LISP  function which does  work this way  is
called nconc.   I  can be  defined in  terms of  a primitive  obscene
function, rplacd.  There are two primitive obscene functions:

rplaca [x ; y] replace the car of x with y.







rplacd [x; y]:  replace the cdr of x with y.








Thus nconc can be defined as:
nconc [x; y] = prog [[z]
	       [null [x] → return [y]];
		z ← x;
	       a[null [cdr [z]] → rplacd [z ; y] return [x]];
	     	z ←cdr [z];
		go [a] ]
These functions  must be  used with  extreme caution.   They are  not
recommended for amateur hacking.  They are introduced here simpley to
show that  it  is  possible to  improve  on the  efficiency  of  pure
algorithms by resorting to these coding tricks.
Consider:

		x ← (NOTHING CAN GO WRONG)
		rplacd [cdddr [x] ; cddr [x]]
		print [x]

So we can use rplacd to generate circular lists (and to generate wall
paper  by printing  them!!) In  general,  to circularize  a non-empty
list, x, rplacd [last [x]; x] suffices where:

		last [x] = [null [cdr [x]] → x; T → last [cdr [x]]]